﻿using System;
using System.Collections.Generic;
using Petra.Gis;
using Petra.Gis.Geometry.Support;
using Petra.Gis.ReferenceSystemServices;
using Petra.Gis.ReferenceSystemServices.Transformations;

namespace Petra.Gis.RenderingServices.PlanarBase 
{
  /// <summary>
  /// represents simple 2d flat(planar) map with projected coordinate system 
  /// that can display gis data as cartesian orthogonal graph 
  /// source data can be anything that supports transformation into map coordinate system
  /// map coordinate system can be either:
  /// planar orthogonal cartesian 
  /// or geographic coords when orthogonal planar projection is applied
  /// no map drawing or rendering methods implemented
  /// </summary>
  public abstract class MapBase
  {
    protected MapLegend _legend;
    protected Renderer _renderer;
    protected MapExtent _extent;
    protected ProjectedCoordinateSystem _mapCRS;
    protected bool _useSpatialFilter;
    protected SRSTransformCache _srsTransformCache;
    
    protected double _metersPerDisplayPaperUnit;
    protected double _metersPerSrsStorageUnit;


    private MapBase(float width, float height, ProjectedCoordinateSystem mapCrs, double nominalMapScale, MapPaperUnit paperOrDisplayUnit)
    {
      this.Width = width;
      this.Height = height;

      //create map legend
      _legend = new MapLegend(this);

      //prepare cache for used transformations
      _srsTransformCache = new SRSTransformCache(this);

      this.ProjectedCoordinateSystem = mapCrs;

      //map scale
      this.NominalScale = nominalMapScale;

      //_renderer = new Renderer(this);

      this.DisplayPaperUnits = paperOrDisplayUnit;

    }




    public MapBase(float width, float height, ProjectedCoordinateSystem mapCrs, double nominalMapScale, MapPaperUnit paperOrDisplayUnit, BoundingBox bounds)
      :this(width, height, mapCrs, nominalMapScale, paperOrDisplayUnit)
    {

      //map extent
      if (bounds == null)
        throw new ArgumentException("bounds null");

      _extent = new MapExtent(this, bounds);

    }




    public MapBase(float width, float height, ProjectedCoordinateSystem mapCrs, double nominalMapScale, MapPaperUnit paperOrDisplayUnit, string mapTitle)
      : this(width, height, mapCrs, nominalMapScale, paperOrDisplayUnit)
    {


      //map extent
      if (_mapCRS.DefaultEnvelope != null)
        _extent = new MapExtent(this, _mapCRS.DefaultEnvelope);
      else
        //no any map extent supplied, create temporary
        _extent = new MapExtent(this, 0, 0, 1, 1);
    }




    protected MapPaperUnit _paperOrDisplayUnit;

    /// <summary>
    /// returns or sets current map display or paper units
    /// such as px, pt, mm, cm, inch,...
    /// </summary>
    public MapPaperUnit DisplayPaperUnits
    {
      get { return _paperOrDisplayUnit; }
      set 
      {

        // prepare map resolution...
        // default is 120 dpi for fast render
        // if pixel and no preference then only 96 dpi
        if (_paperOrDisplayUnit == MapPaperUnits.px)
        {
          //todo: load from preferences
          _pixelPerInch = 96;
        }


        //if display unit (pixel), resolve pixel size
        if (value == MapPaperUnits.px)
        {
          //todo: load dpi def from preferences...
          //now set default 96 dpi
          _metersPerDisplayPaperUnit = 1.0 / 3780;
        }
        else
          _metersPerDisplayPaperUnit = value.MetersPerUnit;

        _paperOrDisplayUnit = value;

      }
    }




    protected int _pixelPerInch = 120;

    /// <summary>
    /// defines map resolution in pixels per inch, 
    /// this is mainly for grid data but also defines screen to projection precision
    /// default value 120 ppi
    /// </summary>
    public int Resolution
    {
      get { return _pixelPerInch; }
      set
        {
          if (value <= 60 || value >= 5000)
            throw new ArgumentException("invalid pixel Per Inch value, must be 60 - 5000");
        }
    }




    public bool ApplyMapRangeAsSpatialFilter
    {
      get { return _useSpatialFilter; }
      set { _useSpatialFilter = value; }
    }



    //must implement in inherited
    protected abstract MapLegend mapLegend { get; }



    public MapLegend Legend
    {
      get { return _legend; }
    }




    public virtual SRSTransformCache SRSTransformCache
    {
      get { return _srsTransformCache; }
    }




    public Renderer CreateRenderer(System.Type geometryType)
    {
      return _renderer;
    }




    protected abstract Renderer mapCreateRenderer(System.Type geometryType);



    //no need to override
    public MapExtent Extent
    {
      get { return _extent; }
      set
      {
        if (value == null)
          throw new ArgumentException("invalid null reference extent", "extent");

        _extent = value;
      }
    }




    //no need to override
    public ProjectedCoordinateSystem ProjectedCoordinateSystem
    {
      get { return _mapCRS; }
      set 
      {
        if (value == null)
          throw new ArgumentException("invalid null reference object", "object");

        //if(!value.Projection.IsRegularOrthogonal)
        //  throw new ArgumentException("this map supports only orthogonal projections");

        if (value.EqualParams(_mapCRS, false))
          _mapCRS = value;
        else
        {
          //clear transform cache
          _srsTransformCache.Clear();
          //todo: clear output map
        }

        // if map projection storage units is angular, then compute
        // radians per meter from defined ellipsoid
        if (_mapCRS.StorageUnit is AngularUnit)
        {
          _metersPerSrsStorageUnit = Math.Sin(((AngularUnit)_mapCRS.StorageUnit).RadiansPerUnit) * (
                _mapCRS.HorizontalDatum.Ellipsoid.SemiMajorAxis *
                _mapCRS.HorizontalDatum.Ellipsoid.AxisUnit.MetersPerUnit);
        }
      }
    }




    //this must be implemented in inherited
    //protected abstract object mapDraw();


    ////empty function for this abstract class
    //public object draw()
    //{ 
    //  return null; //for now...
    //}




    private float _width;


    public float Width
    {
      get { return _width; }
      set
      {
        if (value <= 0)
          throw new ArgumentException("invalid map width specified");

        _width = value;
        this.Resize(_width, _height);
      }
    }




    private float _height;


    public float Height
    {
      get { return _height; }
      set
      {
        if (value <= 0)
          throw new ArgumentException("invalid map width specified");

        _height = value;
        this.Resize(_width, _height);
      }
    }



    public void Resize(float width, float height)
    {

      double lastScale = this.Scale;
      _extent.Resize(width, height, lastScale);

      //rerender whole map content after resize when sized up
      refreshMapContent();
    }




    double _nominalScale;


    public double NominalScale
    {
      get { return _nominalScale; }
      set
      {
        if (value < 1)
          throw new ArgumentException("invalid scale: " + value.ToString());
        _nominalScale = value;

        //refresh map content...
        refreshMapContent();
      }
    }




    protected void refreshMapContent()
    {
      //render in bottm to top order !!
      for (int i = this.Legend.DisplayOrder.Count - 1; i >= 0; i--)
        this.Legend.DisplayOrder[i].RenderData();

    }


    public double Scale
    {
      get
      {
        return (this.Width * _metersPerDisplayPaperUnit) / (this.Extent.Width * _metersPerSrsStorageUnit); 
      }

      set 
      {
        this.Extent.Resize(this.Width, this.Height, value);
      }
    }



  }
}
